/*
 * <one line to give the program's name and a brief idea of what it does.>
 * Copyright (C) 2019
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "dblmdb.hpp"

#include "logger.h"

#include "lmdb.h"
#include <spdlog/spdlog.h>
#include <thread>

/*******************
 * LMDB
 ******************/

std::shared_ptr<spdlog::logger> LMDB::Log = nullptr;
std::map<envData, uint8_t > LMDB::m_Environments;

MDB_env * LMDB::init(const std::string & path) {
	MDB_env * result = nullptr;

	// logger first
	if(!Log)
		Log = Logger::get("LMBD");

	// try to find if we already open environment
	for(auto & i : m_Environments) {
		if(i.first.first == path) {
			Log->debug("Use opened environment with path '{}'", path);
			result = i.first.second;
			i.second++;
			break;
		}
	}

	// opened environment not found
	if(! result) {
		int ret = mdb_env_create(&result);

		if(!ret) {
			LMDB::Log->debug("Environment created with path '{}'", path);
			// TODO correct maxsize?
			ret = mdb_env_set_mapsize(result, (size_t)1048576000);

			if(ret) {
				LMDB::Log->error("Error set map size to env #{}: {}", ret, errorMessage(ret));
			}

			// TODO только 1 база на среду, большего нам не понадобиться
			// set max dbs limited by threads
// 			if((ret = mdb_env_set_maxdbs(Result, std::thread::hardware_concurrency() )) != 0) {
// 				LMDB::Log->error("Error set map size to env #{}: {}", ret, errorMessage(ret));
// 			}

			// open env
			ret = mdb_env_open(result, path.c_str(),  MDB_NOMETASYNC | MDB_NOSYNC | MDB_MAPASYNC | MDB_WRITEMAP, 0664);

			if(ret) {
				LMDB::Log->error("Error opening environment with path '{}' #{}: {}", path, ret, errorMessage(ret));
				mdb_env_close(result);
				result = nullptr;
			} else {
				// all done. we can return this pointer
				LMDB::Log->debug("Environment opened with path '{}'", path);
				m_Environments[ {path, result}] = 1;
			}
		} else {
			LMDB::Log->error("Error create env #{}: {}", ret, errorMessage(ret));
		}
	}

	return result;
}

void LMDB::shutdown(MDB_env * env) {
	for(auto & i : m_Environments) {
		if(i.first.second == env) {
			i.second --;

			if(i.second == 0) {
				LMDB::Log->debug("Environment closed with path '{}'", i.first.first);
				mdb_env_close(i.first.second);
				m_Environments.erase(i.first);
			}
		}

		break;
	}
}

std::string LMDB::errorMessage(int error) {
	return mdb_strerror(error);
}

/*******************
 * LMTxn
 ******************/
LMTxn::LMTxn(MDB_env * environment) {
	int ret = mdb_txn_begin(environment, nullptr, 0, &m_Transaction);

	switch(ret) {
		case MDB_SUCCESS:
			break;

		default:
			LMDB::Log->error("Error begin transaction #{}: {}", ret, LMDB::errorMessage(ret));
	}
}

LMTxn::~LMTxn() {
	// NOTE abort transaction only if pointer valid!
	if(m_Transaction) {
		abort();
	}
}

void LMTxn::commit() {
	int ret = mdb_txn_commit(m_Transaction);

	switch(ret) {
		case MDB_SUCCESS:
			m_Transaction = nullptr;
			break;

		default:
			LMDB::Log->error("Error commit transaction #{}: {}", ret, LMDB::errorMessage(ret));
	}
}

void LMTxn::abort() {
	mdb_txn_abort(m_Transaction);
	m_Transaction = nullptr;
}

/*******************
 * LMDBi
 ******************/

LMDBi::LMDBi(LMTxn * txn, unsigned int flags) {
	int ret = mdb_dbi_open(txn->raw(), nullptr, flags, &m_DBi);

	switch(ret) {
		case MDB_SUCCESS:
			break;

		default:
			LMDB::Log->error("Error opening database #{}: {}", ret, LMDB::errorMessage(ret));
	}
}

void LMDBi::erase(LMTxn * txn, bool close) {
	if(m_DBi) {
		int ret = mdb_drop(txn->raw(), m_DBi, close ? 1 : 0);

		switch(ret) {
			case MDB_SUCCESS:
				break;

			default:
				LMDB::Log->error("Error erase database #{}: {}", ret, LMDB::errorMessage(ret));
		}
	}
}

/*******************
 * DBLMBD
 ******************/

DBLMDB::DBLMDB(const std::string & dbname) : DataBase() {
	m_Env = LMDB::init(dbname);

	if(!m_Env) {
		LMDB::Log->error("Dont get LMDB instance for path '{}'", dbname);
	}
}

DBLMDB::~DBLMDB() {
	LMDB::shutdown(m_Env);
}

bool DBLMDB::empty() {
	bool result = true;
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_stat stats;
			int ret = mdb_stat(txn.raw(), dbi.raw(), &stats);

			switch(ret) {
				case MDB_SUCCESS:
					result = stats.ms_entries == 0;
					break;

				default:
					LMDB::Log->error("Failed got database stats #{}: {}", ret, LMDB::errorMessage(ret));
			}
		}
	}

	return result;
}

void DBLMDB::erase() {
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			dbi.erase(&txn);
			txn.commit();
		}
	}
}

std::size_t DBLMDB::size() {
	std::size_t result = 0;

	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_stat stats;
			int ret = mdb_stat(txn.raw(), dbi.raw(), &stats);

			switch(ret) {
				case MDB_SUCCESS:
					result = stats.ms_entries;
					break;

				default:
					LMDB::Log->error("Failed got database stats #{}: {}", ret, LMDB::errorMessage(ret));
			}
		}
	}

	return result;
}

bool DBLMDB::get(void * data, const std::size_t & size) {
	bool result = false;
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_cursor * cursor;
			MDB_val key, value;
			int ret = mdb_cursor_open(txn.raw(), dbi.raw(), &cursor);

			if(!ret) {
				ret = mdb_cursor_get(cursor, &key, &value, MDB_FIRST);

				if(!ret) {
					// copy data
					std::memcpy(data, key.mv_data, key.mv_size);

					// DO NOT copy value, only key
					// delete from db
					if(mdb_cursor_del(cursor, 0) == MDB_SUCCESS) {
						result = true;
					}
				} else {
					LMDB::Log->error("FAILED get by CURSOR! #{}: {}", ret, LMDB::errorMessage(ret));
				}
			} else {
				LMDB::Log->error("FAILED OPEN CURSOR! #{}: {}", ret, LMDB::errorMessage(ret));
			}

			txn.commit();
		}
	}

	return result;
}

void DBLMDB::put(void * data, const std::size_t & size) {
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_val key, value;
			key.mv_data = data;
			key.mv_size = size;
			value.mv_data = data;
			value.mv_size = 0;
			mdb_put(txn.raw(), dbi.raw(), &key, &value, MDB_NOOVERWRITE);
			txn.commit();
		}
	}
}

std::size_t DBLMDB::addBuilds(std::vector<PathData> * data, PathData * build, DataBase * queue) {
	std::size_t count = 0;
	int ret;
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_val key, value;
			// key and value size unchanged for data and build, no need set it multiple times
			key.mv_size = sizeof(PlayerBuild);
			value.mv_size = sizeof(PathData::ai);

			// save calculated build
			// NOTE key = PlayerBuild, value = ai, immater+ultium ignore here.
			if(build) {
				key.mv_data = build->ship.data;
				value.mv_data = build->ai;

				ret = mdb_put(txn.raw(), dbi.raw(), &key, &value, 0);

				if(ret) {
					LMDB::Log->error("Failed update build #{}: {}", ret, LMDB::errorMessage(ret));
				}
			}

			// check new build, drop already exists!
			for(auto & i : *data) {
				key.mv_data = i.ship.data;
				value.mv_data = i.ai;
				ret = mdb_put(txn.raw(), dbi.raw(), &key, &value, MDB_NOOVERWRITE);

				switch(ret) {
					case MDB_SUCCESS:
						// its new record!
						queue->put(&i, sizeof(i));
						count++;
						break;

					case MDB_KEYEXIST:
						break;

					default:
						LMDB::Log->error("Error add build #{}: {}", ret, LMDB::errorMessage(ret));
				}
			}

			txn.commit();
		}
	}

	return count;
}

bool DBLMDB::updateBuild(PathData * data) {
	bool success = false;
	int ret;
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_val key, value;
			key.mv_size = sizeof(PathData::ship);
			value.mv_size = sizeof(PathData::ai);

			key.mv_data = data->ship.data;
			value.mv_data = data->ai;

			ret = mdb_put(txn.raw(), dbi.raw(), &key, &value, 0);

			if(ret) {
				LMDB::Log->error("Failed update build #{}: {}", ret, LMDB::errorMessage(ret));
			} else
				success = true;

			txn.commit();
		}
	}

	return success;
}

bool DBLMDB::getBuild(PathData * build) {
	LMTxn txn(m_Env);
	bool success = false;

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_cursor * cursor;
			MDB_val key, value;
			int ret = mdb_cursor_open(txn.raw(), dbi.raw(), &cursor);

			switch(ret) {
				case MDB_SUCCESS:
					ret = mdb_cursor_get(cursor, &key, &value, MDB_FIRST);

					if(ret == MDB_SUCCESS) {
						// key = ship data
						// value = min and max ai levels
						std::memcpy(build->ship.data, key.mv_data, key.mv_size);
						std::memcpy(build->ai, value.mv_data, value.mv_size);

						// drop row
						if(mdb_cursor_del(cursor, 0) != MDB_SUCCESS) {
							LMDB::Log->error("fail delete record by cursor #{}: {}", ret, LMDB::errorMessage(ret));
						} else
							success = true;

						txn.commit();
					} else if(ret == MDB_NOTFOUND) {
						LMDB::Log->debug("Database empty");
					} else {
						LMDB::Log->error("fail get value by cursor #{}: {}", ret, LMDB::errorMessage(ret));
					}

					break;

				default:
					LMDB::Log->error("fail open cursor #{}: {}", ret, LMDB::errorMessage(ret));
					break;
			}
		}
	}

	return success;
}

std::size_t DBLMDB::removeZeroWins() {
	std::size_t count = 0;
	LMTxn txn(m_Env);

	if(txn.ready()) {
		LMDBi dbi(&txn);

		if(dbi.open()) {
			MDB_cursor * cursor;
			MDB_val key, value;
			uint16_t data[2];

			int ret = mdb_cursor_open(txn.raw(), dbi.raw(), &cursor);

			if(ret == MDB_SUCCESS) {
				ret = mdb_cursor_get(cursor, &key, &value, MDB_FIRST);

				auto check = [&]() -> void {
					std::memcpy(data, value.mv_data, value.mv_size);

					if(data[0] == data[1]) {
						if(mdb_cursor_del(cursor, 0) == MDB_SUCCESS)
							count++;
					}
				};

				if(ret == MDB_SUCCESS) {
					check();

					while((ret = mdb_cursor_get(cursor, &key, &value, MDB_NEXT)) == MDB_SUCCESS) {
						check();
					}

					if(ret != MDB_SUCCESS && ret != MDB_NOTFOUND) {
						LMDB::Log->error("fail iterate storage #{}: {}", ret, LMDB::errorMessage(ret));
					}
				} else {
					LMDB::Log->error("fail get first element #{}: {}", ret, LMDB::errorMessage(ret));
				}

				txn.commit();
			} else {
				LMDB::Log->error("fail open cursor #{}: {}", ret, LMDB::errorMessage(ret));
			}
		}
	}

	return count;
}
